/******************************************************************************
 * XWiki.JavaScriptExtension\[0\]: JavaScript with Velocity Substitutions
 * (parse content: yes)
 * Copyright (C) 2009-2011, Niels Mayer. All Rights Reserved. http://nielsmayer.com
 ******************************************************************************/

//#set( $instruction_txt = "Select a feed below, view by clicking on feed entry in Timeline above..." )##
//#set( $instruction_img = $xwiki.getSkinFile("icons/msginfo.png")    )##
//#set( $error_img       = $xwiki.getSkinFile("icons/msgerror.png")   )##
//#set( $warning_img     = $xwiki.getSkinFile("icons/msgwarning.png") )##
//#set( $loading_img     = $xwiki.getSkinFile("icons/spinner.gif")    )##

/****************************************************************************
 * Parameters needed by Macros.Exhibit
 ****************************************************************************/
var exhibit_views         = "map,timeline";
var exhibit_gmapkey       = "ABQIAAAAdVnw129294zmeOMDx36lVBT6GRSDIsqd3PmO-Nw2RjBxOmqGtxSVarVhAKwPoZgBA2fzIOTomgJa3g";
//var feed_map            = new Hash(); //http://www.prototypejs.org/api/hash
var current_feed_episodes = []; //set iff Macros.Feed3ExhibitJSON success
var current_feed_length   = null;
var current_feed_idx	  = null;
var playlist 		  = [];  // playlist for jwplayer sent via LOAD in player_load_and_play()
var playlist_changed	  = false;
var playlist_appended     = false;
var plentry_base	  = 1000;
var plentry_len		  = null;
var plentry_idx		  = null;  //updated by player_item_listener() 
/****************************************************************************
 * empty function (for now) called by 
 * ts.ExhibitOverridesJS:JSX0:addExhibitInterceptorsAndOverides()
 * just prior to Exhibit instantiating it's first display. It is normally
 * used to add dynamic data to the otherwise static JSON 'data' var defined
 * in JSX1-12 below...
 *****************************************************************************/
function setApplicationDataToExhibitDataItems() {
}
/****************************************************************************
 * 
 *****************************************************************************/
function insertHeadPlaylist() {
}
/****************************************************************************
 * insert JWplayer playlist entry at playlist[plentry_idx] into exhibit DB
 *****************************************************************************/
function insertTailPlaylist() {
  var ntree = [
    {id:		'plentry'+plentry_len,
     label:		playlist[plentry_len].title, //note: .label name (for title) needed by exhibit
     type:		'plentry', //.type value needed by exhibit: note entryType below, which is playlist[plentry_len].type
     entryType:		playlist[plentry_len].type, 
     tags:		playlist[plentry_len].tags,
     file:		playlist[plentry_len].file,
     link:		playlist[plentry_len].link,
     image:		playlist[plentry_len].image,
     author:		playlist[plentry_len].author,
     description:	playlist[plentry_len].description,
     start:		playlist[plentry_len].start,
     duration:		playlist[plentry_len].duration,
     date:	        playlist[plentry_len].date
    }];
  // exhibit.getDatabase().loadItems(ntree, "$xwiki.getURL("$doc.fullName", "view", "")");
  exhibit.getDatabase().loadItems(ntree, "???"); // TODO: qtzibit
}
/****************************************************************************
 * 
 *****************************************************************************/
function insertCurPlaylist() {
}
/****************************************************************************
 * returns back to showing  facets and viewpanel at 80%
 * KNOWN-BUG: when you change 80% in the stylesheet, it won't get changed here.
 *****************************************************************************/
function normal_width_viewpanel_for_browsing() {
  // save the current position of player, so that when we switch back, it starts playing where left off..
  if (player_posn) {		// if player_position_listener() fired
    if (player_status_cur == "COMPLETED") {//if COMPLETED, position is probably end, 
      playlist[plentry_idx].start = 0.0;   //then set .start to beginning.
    }
    else {
      playlist[plentry_idx].start = player_posn;
    }
    playlist[plentry_idx].duration = player_durn;
    playlist_changed = true;
    //TODO: this also happens when playback already stopped, message below can be misleading.
    statusMsg("Player exit: click 'PLAYPOD' viewpanel to resume playback at " + player_posn.toFixed(1) + "s./"+  player_durn.toFixed(1) + "s.");
  }
  //put all the podcast facets back
  SimileAjax.jQuery('#podcasts-search-facet')[0].style.display = "block";
  SimileAjax.jQuery('#podcasts-topic-facet')[0].style.display = "block";
  SimileAjax.jQuery('#podcasts-producer-facet')[0].style.display = "block";
  SimileAjax.jQuery('#podcasts-video-facet')[0].style.display = "block";
  SimileAjax.jQuery('#podcasts-failed-facet')[0].style.display = "block";
  SimileAjax.jQuery('#podcasts-fetched-facet')[0].style.display = "block";
  var rightcol_elt = SimileAjax.jQuery('div#browse-feeds-and-playback')[0];
  rightcol_elt.style.width = '80%';
  //just switched away from player viewpanel, a new tab "button" was rendered,
  //give it a new onclick, so that when player viewpanel tab is clicked, player
  //is correctly init'd (else you just end up with the div where jwplayer was supposed to go).
  setTimeout (function () {
		checkForIdAndRun(50, 100, 'div#podcasts-viewpanel span.exhibit-viewPanel-viewSelection-view', 
				 function () { 
				   var elts = SimileAjax.jQuery('div#podcasts-viewpanel span.exhibit-viewPanel-viewSelection-view');
				   elts[elts.length - 1].onclick = player_init_auto;
				 });
	      }, 1000); //initially wait 1 sec after load before checking every 100ms up to 50x
}
/****************************************************************************
 * makes the column created by rightcol_start("browse-feeds-and-playback")
 * 100% width, and temporarily sets display:none on any facets that might
 * be in the way to make room
 *****************************************************************************/
function full_width_viewpanel_for_playback() {
  //remove all the podcasts facets...
  SimileAjax.jQuery('#podcasts-search-facet')[0].style.display = "none";
  SimileAjax.jQuery('#podcasts-topic-facet')[0].style.display = "none";
  SimileAjax.jQuery('#podcasts-producer-facet')[0].style.display = "none";
  SimileAjax.jQuery('#podcasts-video-facet')[0].style.display = "none";
  SimileAjax.jQuery('#podcasts-failed-facet')[0].style.display = "none";
  SimileAjax.jQuery('#podcasts-fetched-facet')[0].style.display = "none";
  var rightcol_elt = SimileAjax.jQuery('div#browse-feeds-and-playback')[0];
  rightcol_elt.style.width = '100%';
  //
  setTimeout (function () {
		checkForIdAndRun(50, 100, 'div#podcasts-viewpanel span.exhibit-viewPanel-viewSelection-view', 
				 function () { 
				   var elts = SimileAjax.jQuery('div#podcasts-viewpanel span.exhibit-viewPanel-viewSelection-view');
				   for ( var i = 0 ; (i < elts.length) ; i++) {
				     if (typeof(elts[i].onclick) != "function")
				       elts[i].onclick = normal_width_viewpanel_for_browsing;
				   }
				 });
	      }, 1000); //initially wait 1 sec after load before checking every 100ms up to 50x
}
/****************************************************************************
 * callback fires when position in playlist changes
 *****************************************************************************/
var countspacula = 0;
function player_item_listener(obj) {
  if (plentry_idx != null) {
    if (player_durn && (playlist[plentry_idx].duration != player_durn)) {
      playlist[plentry_idx].start = 0.0; //then set .start to beginning.
      playlist[plentry_idx].duration = player_durn;
      playlist_changed = true;
    }
    if (playlist_changed)
      player_load_and_play(true); //load any potential updates to playlist, continue playing

//     switch (player_status_cur) {
//     case 'COMPLETED': //completed playing, or never played
//       switch (player_status_prev) {
//       case 'BUFFERING':
//       case 'PLAYING':
//       case 'IDLE':
// 	if (player_durn && (playlist[plentry_idx].duration != player_durn)) {
// 	  playlist[plentry_idx].start = 0.0; //then set .start to beginning.
// 	  playlist[plentry_idx].duration = player_durn;
// 	  playlist_changed = true;
// 	}
// 	if (playlist_changed)
// 	  player_load_and_play(true); //load any potential updates to playlist, continue playing
// 	break;
//       }
//       break;
//     case 'IDLE':  // idle -- this seems to fire, and if you call player_load_and_play(), it infinite-loops
//       switch (player_status_prev) {
//       case 'BUFFERING':
//       case 'PLAYING':
// 	if (player_posn && (playlist[plentry_idx].start != player_posn)) {
// 	  playlist[plentry_idx].start = player_posn; //set start to latest position.
// 	  playlist[plentry_idx].duration = player_durn;
// 	  playlist_changed = true;
// 	}
// 	if (playlist_changed)
// 	  player_load_and_play(true);  // if was playing, now stopped, load PL, start
// 	break;
//       }
//     case 'PAUSED': //paused means there's a valid .start, but don't start playback on new playlist 
//       switch (player_status_prev) {
//       case 'BUFFERING':
//       case 'PLAYING':
//       case 'IDLE':
// 	if (player_posn && (playlist[plentry_idx].start != player_posn)) {
// 	  playlist[plentry_idx].start = player_posn; //set start to latest position.
// 	  playlist[plentry_idx].duration = player_durn;
// 	  playlist_changed = true;
// 	}
// 	if (playlist_changed)
// 	  player_load_and_play(false); //load new playlist, don't play
// 	break;
//       }
//       break;
//     case 'BUFFERING':
//     case 'PLAYING':  // if playing, but this callback fired, we're moving to a difft item, last chance to update...
//       switch (player_status_prev) {
//       case 'BUFFERING':
//       case 'PLAYING':
//       case 'IDLE':
// 	if (player_posn && (playlist[plentry_idx].start != player_posn)) {
// 	  playlist[plentry_idx].start = player_posn; //set start to latest position.
// 	  playlist[plentry_idx].duration = player_durn;
// 	  playlist_changed = true;
// 	}
// 	if (playlist_changed)
// 	  player_load_and_play(true);
// 	break;
//       }
//       break;
//     }
  }
  var oldidx = plentry_idx;
  if (playlist_appended) 
    plentry_idx = obj.index + 1;  // jwplayer has bug w/r/t index being wrong if appending last item? this fixes?
  else
    plentry_idx = obj.index;

  statusMsg((countspacula++)
	    + ':'
	    + player_status_cur
	    + '/'
	    + player_status_prev
	    + ": item "
	    + (plentry_idx+1)
	    + "/"
	    + plentry_len
	    + ": '"
	    + playlist[plentry_idx].title
	    + "' "
	    + playlist[plentry_idx].start.toFixed(1)
	    + "/"
	    + playlist[plentry_idx].duration.toFixed(1)
	    + " Prev: "
	    + playlist[oldidx].start.toFixed(1)
	    + "/"
	    + playlist[oldidx].duration.toFixed(1)
	    );
}
/****************************************************************************
 * callback fires when player changes state. IDLE means nothing loaded, ready
 * to load, COMPLETED means current file has finished playing.
 *****************************************************************************/
var player_status_cur = null;
var player_status_prev = null;
function player_state_listener(obj) { //IDLE, BUFFERING, PLAYING, PAUSED, COMPLETED
  player_status_cur = obj.newstate; 
  player_status_prev = obj.oldstate; 

//  statusMsg("Player "
//	    + player_status_cur
//	    + " (was "
//	    + player_status_prev
//	    + ") playlist item "
//	    + (plentry_idx+1)
//	    + "/"
//	    + plentry_len
//	    + ": '"
//	    + playlist[plentry_idx].title
//	    + "' "
//	    + playlist[plentry_idx].start.toFixed(1)
//	    + "/"
//	    + playlist[plentry_idx].duration.toFixed(1)
//	    );

  //if new playlist entries added while playing, add them after stopping or completing
  //(updates while playing has issues in that it forces the current media to reload and re-seek 
  //to last playing position: works fine for small audio files (already in file-cache?), 
  //doesn't work at all for larger video files from some servers,
  //(which require lengthy re-download to the seekpoint). Thus, this "solution":
  switch (player_status_cur) {
  case 'PAUSED':
    // do nothing on pause don't disrupt continued playback w/ potential reload
    break;
  case 'COMPLETED':
  case 'IDLE':
    switch (player_status_prev) {
    case 'BUFFERING':
    case 'PLAYING':
      if (playlist_changed)
	player_load_and_play(true);  // if was playing, now stopped, load PL, start
      break;
    }
    break;
    //  case 'BUFFERING':
    //  case 'PLAYING':
    //    switch (player_status_prev) {
    //    case 'COMPLETED':
    //    case 'PAUSED':
    //      if (playlist_changed)
    //	player_load_and_play(true);  // if was stopped, now playing, load PL, and play.
    //      break;
    //    }
    //    break;
  }
}
/****************************************************************************
 * update position as media is played. Just update a global because anything
 * more seems to cause jerky video-playback on slower machines.
 * in normal_width_viewpanel_for_browsing() is called when switching
 * away from the playback viewpanel, that's when player_posn gets set
 * as playlist[plentry_idx].start .
 *****************************************************************************/
var player_loading = true;
var player_seek = null;
var player_posn = null;
var player_durn = null;
function player_position_listener(obj) { 
  if (obj.position > 0) {
    player_posn = obj.position;
    player_durn = obj.duration;
    if (player_loading) {	 // works alongside autoplay: true
      the_jw_obj.sendEvent('PLAY', 'false');  // pause to give it time to load properly
      player_loading = false;

      if (playlist[plentry_idx].duration != player_durn) {
	playlist[plentry_idx].duration = player_durn;
	playlist_changed = true;
      }

      if (playlist[plentry_idx].start) {
	setTimeout (function () {
		      the_jw_obj.sendEvent('SEEK', playlist[plentry_idx].start);
		      the_jw_obj.sendEvent('PLAY', 'true');
		    }, 100);
      }
      else {
	setTimeout (function () {
		      the_jw_obj.sendEvent('PLAY', 'true');
		    }, 100);
      }
    }
  }
}
/****************************************************************************
 * passed as callback parameter to Macro.JWPlayer's jwplayer_create_generic()
 * It is called after JWPlayer has initialized to load data or playlists.
 *****************************************************************************/
var the_jw_obj = null;
function init_jspl_and_listeners(jw_obj) {
  player_loading = true;
//9/17/10: Note hack needed by chome6/konqueror/webkit or perhaps latest
//5.2 jwplayer. The line below, provided by jwplayer which is the caller
//of this function, sets the wrong object. It appears to be the HTML object,
//rather than the flashplayer. The hack is to just lookup the id directly...
//  the_jw_obj = jw_obj;                                  //global used by other fns
  the_jw_obj = SimileAjax.jQuery('object#jwplayerBrowsePodcasts.xwiki-JWPlayer')[0]; 
  the_jw_obj.addControllerListener("ITEM", "player_item_listener");
  //the_jw_obj.addControllerListener("MUTE", "muteListener");
  //LOADED causes serious performance issues; controlbar already shows amount loaded graphically
  //so this is superfluous, enable (to see perf hit) via loadedListenerToggle()
  ////the_jw_obj.addModelListener("LOADED", "loadedListener");
  the_jw_obj.addModelListener("STATE", "player_state_listener");    //sets globals player_status_cur, player_status_prev.
  the_jw_obj.addModelListener("TIME",  "player_position_listener"); //sets globals player_posn, player_durn
  //the_jw_obj.addViewListener("VOLUME", "volumeListener");
  player_load_and_play(true);
}
/****************************************************************************
 * 
 ****************************************************************************/
function checkForIdAndRun (check_count, timeout, css_spec, run_fn) {
  var elts = SimileAjax.jQuery(css_spec);
  if ( elts.length > 0) {     //if it matches, run the closure
    run_fn();
  }
  else { //if no match, set timeout to run again, halting after 'check_count' attempts
    if (check_count > 0) {
      setTimeout (function () {checkForIdAndRun(--check_count, timeout, css_spec, run_fn);}, timeout);
    }
    else {
      alert("checkForIdAndRun(): timed out waiting for instantiation of '" + css_spec + "'");
    }
  }
}
/****************************************************************************
 * NOTE: This will need to change if parent hierarchy of episode-bubble changes
 *
 * the SPAN parent of button w/ onclick that called processEpisodeThumBtnClick(),
 * created by Episodes-collection thumbnail-view, is given attribute ex:itemID,
 * the value of which is the episodeID. path from bubble-containing itemID to 
 * button: span/table/tbody/tr/td/button
 ****************************************************************************/
function get_episodeID_from_episode_lens_button(callingWidget) {
  return (callingWidget.parentNode.parentNode.getAttribute("ex:itemID"));
}
/****************************************************************************
 *
 ****************************************************************************/
function get_podcastID_from_episode_lens_button(callingWidget) {
  return (exhibit.getDatabase().getObject(get_episodeID_from_episode_lens_button(callingWidget),
					  "podcast"));
}
/****************************************************************************
 * TODO: need to match on specific producers and have a producer-based
 * callback which has it's own local way of extracting media from given
 * producer, e.g. youtube producer superclass would handle "case 'YOUTUBE:'
 ****************************************************************************/
function get_media_from_episode_lens_button(callingWidget) {
  return (exhibit.getDatabase().getObject(get_episodeID_from_episode_lens_button(callingWidget),
					  "entryURI"));
}
/****************************************************************************
 *
 ****************************************************************************/
function get_description_from_episode_lens_button(callingWidget) {
  return (exhibit.getDatabase().getObject(get_episodeID_from_episode_lens_button(callingWidget),
					  "entryDesc"));
}
/****************************************************************************
 *
 ****************************************************************************/
function get_title_from_episode_lens_button(callingWidget) {
  return (exhibit.getDatabase().getObject(get_episodeID_from_episode_lens_button(callingWidget),
					  "label"));
}
/****************************************************************************
 *
 ****************************************************************************/
function get_date_from_episode_lens_button(callingWidget) {
  return (exhibit.getDatabase().getObject(get_episodeID_from_episode_lens_button(callingWidget),
					  "entryTime"));
}
/****************************************************************************
 * TODO: this should actually give link back to HTML page for "episode",
 * alternatey lookup "podcastID" assoc'd w/ episode, then
 * lookup 'fetchedLink' in the "Podcast" collection.
 * For now, use whatever podcaast gives as it's link, even though this
 * should actually be a link back to the html assoc'd w/ media.
 ****************************************************************************/
function get_link_from_episode_lens_button(callingWidget) {
  return (exhibit.getDatabase().getObject(get_podcastID_from_episode_lens_button(callingWidget),
					  "fetchedLink"));
}
/****************************************************************************
 *
 ****************************************************************************/
function get_image_from_episode_lens_button(callingWidget) {
  //TODO: some logic whether to use fetchedImg or Img
  return (exhibit.getDatabase().getObject(get_podcastID_from_episode_lens_button(callingWidget),
					  "fetchedImg"));
}
/****************************************************************************
 *
 ****************************************************************************/
function get_author_from_episode_lens_button(callingWidget) {
  return (exhibit.getDatabase().getObject(get_podcastID_from_episode_lens_button(callingWidget),
					  "producer"));
}
/****************************************************************************
 *
 ****************************************************************************/
function get_tags_from_episode_lens_button (callingWidget) {
  return ([ exhibit.getDatabase().getObject(get_podcastID_from_episode_lens_button(callingWidget),
					  "topic") ] );
}
/****************************************************************************
 * used to construct JSON playlist entry "{ type: get_type_from_media(media) }"
 * The following default types are supported by JWPlayer 4.4:
 * >> video: progressively downloaded FLV / MP4 video, but also AAC audio.
 * >> sound: progressively downloaded MP3 files.
 * >> image: JPG/GIF/PNG images.
 * >> youtube: videos from Youtube.
 * TODO: http: FLV/MP4 videos played as http speudo-streaming.
 * TODO: rtmp: FLV/MP4/MP3 files played from an RTMP server.
 ****************************************************************************/
function get_type_from_media(episode_media) {
  switch (getFileExtension(episode_media)) {
  case '.mp3':
  case '.aac': 
    return ('sound');
    break;
  case '.m4v':
  case '.mp4':
  case '.flv':
    return ('video');
    break;
  case 'YOUTUBE':
    return ('youtube');
    break;
  case '.jpg':
  case '.gif':
  case '.png':
    return ('image');
    break;
  default:
    return (null);
    break;
  }
}
/****************************************************************************
 * construct a jwplayer playlist entry in JS, including type of item. This will
 * make youtube work -- base type off get_type_from_media(playlist[plentry_idx].file),
 * playlist is an array of obj, where obj is defined in it's use as parameter:
 *   function loadFile(obj)
 *   function addItem(obj, idx)

 * X obj: duration:     number - seconds for rotate time
 * X obj: start:        number - seconds
 * X obj: description:  text
 * X obj: link:         url    - url for link
 *   obj: id:           text   - unique id of the media file
 * X obj: tags:		text - category
 *   obj: audio         url
 *   obj: captions:     url    - url for captions file
 * X obj: image:        url    - url for image file
 * X obj: type:         text   - media type (mp3, flv, rtmp, jpg, png, gif, swf, rbs, 3gp, mp4, mv4)
 * X obj: file:         url    - single media file, stream, or playlist (can be playlist generator script or redirect)
 * X obj: title:        text   - title of the media file
 * X obj: author:       text   - author's name
 * 
 *   idx: number - position in playlist array (integer starting with 0)
 ****************************************************************************/
function player_load_and_play(play_p) {
  setTimeout (function () {
		if (playlist_changed) {
		  the_jw_obj.sendEvent("LOAD", playlist);
		  playlist_changed = false;
		  playlist_appended = false;
		}
		the_jw_obj.sendEvent('ITEM', plentry_idx);
		// Now done in player_position_listener()
//		if (playlist[plentry_idx].start) {
//		  the_jw_obj.sendEvent('SEEK', playlist[plentry_idx].start);
//		}
//		else {
//		  the_jw_obj.sendEvent('PLAY', 'true');
//		}
	      }, 0);
}
/****************************************************************************
 * #set( $jw_name = "jwplayer$doc.name" )##
 * #jwplayer_in_exhibit_lens_template("$jw_name", "player_init('$jw_name');")##
 ****************************************************************************/
var player_init_count = 0;
var player_name;
function player_init_auto() {
  player_init(player_name);
}
function player_init(idname) {
  player_name = idname; //on the first call, out of the template instantiationm, set the name of the player and div into which the player swfobject is substituted, subsequent non-template calls to player_init_auto() will then work
  the_jw_obj = null; //reset;  any old value (leftover from previous viewpanel) invalid/destroyed; new value valid after init_jspl_and_listeners() callback fires.
  player_posn = null; //reset position tracking, see player_position_listener() and normal_width_viewpanel_for_browsing()
  player_durn = null; //ditto
  if (player_init_count > 0) { //don't fire from the template.... (there should be a better way of testing for this)
    // make the player full width
    full_width_viewpanel_for_playback();
    var divs = SimileAjax.jQuery('div.'+idname);
    if (divs.length > 0) {	// this case is only true the very first time the player is created, since it's in a viewpanel, there's no id, need to force name/id for swfobject to substitute properly.
      var divToReplace  = divs[0];
      divToReplace.id   = divToReplace.name = idname; //"jwplayer$doc.name"; 
    }
    //subsequent player instantiations past the first, the above query no
    //longer matches, (there's no div, just the leftover object that housed
    //the now defunct jwplayer from the last viewpanel?) however, it seems
    //to re-substitute itself into the old object (named previously) just
    //fine when you flip back to the viewpanel....
    jwplayer_embed_generic(idname,
			   {autostart:			"true", //force loading ASAP, but pause: see player_position_listener()
			    playlist:			"right",
			    playlistsize:		"350",
			    shuffle:			"false",
			    repeat:			"list",
			    dock:                       "true",
			    stretching:			"uniform", //"none" doesn't work well for YT videos
			    displaytitle:		"true",
			    plugins:			"shortcuts-1", //metaviewer-1, --> only works for mp4/flv
//			    'metaviewer.position':	'left',
//			    'metaviewer.size':		'200',
			    'shortcuts.slowmotion':	'true',	 //nb:shortcuts-1 binds to ':'
			    'shortcuts.slowmotionvolume': '50'}, //slowmo at 50% volume
			   init_jspl_and_listeners, //callback that sets the_jw_obj, computes playlist, current media, and starts playing
			   "100%",
			   "395px", //((isAudioP(playlist[plentry_idx].file)) ? "30px" : "395px"),
			   null);
  }
  else {
    // this branch is fired on first load, it's from the template, and none of the Exhibit
    // UI widgetry has been created yet...
    // on first load add onclick to last viewpanel (media) to call this fn
    // to create the player....
    setTimeout (function () {
		  checkForIdAndRun(50, 100, 'div#podcasts-viewpanel span.exhibit-viewPanel-viewSelection-view', 
				   function () { 
				     var elts = SimileAjax.jQuery('div#podcasts-viewpanel span.exhibit-viewPanel-viewSelection-view');
				     elts[elts.length - 1].onclick = player_init_auto;
				   });
		}, 1000); //initially wait 1 sec after load before checking every 100ms up to 50x
  }
  player_init_count++;
}
/****************************************************************************
 *
 ****************************************************************************/
function player_extract_from_episode_lens_button(callingWidget) {
  //youtube feed URL needs substitution to actually work as media-file in JWplayer
  var plfl = get_media_from_episode_lens_button(callingWidget);
  if ((getFileExtension(plfl) == 'YOUTUBE') && (plfl.toLowerCase().indexOf("http://www.youtube.com/watch?") < 0)) {
    plfl = "http://www.youtube.com/watch?v=" + getFileName(plfl); 
  }
  playlist[plentry_len] = { //nb: extending playlist by one item (playlist[plentry_len-1] was old end)
    title:	get_title_from_episode_lens_button(callingWidget),
    file:	plfl,
    type:	get_type_from_media(plfl),
    tags:	get_tags_from_episode_lens_button(callingWidget),
    link:       get_link_from_episode_lens_button(callingWidget),
    image:      get_image_from_episode_lens_button(callingWidget),
    author:     get_author_from_episode_lens_button(callingWidget),
    description:get_description_from_episode_lens_button(callingWidget),
    date:       get_date_from_episode_lens_button(callingWidget),
    start:	0.0, // when getting episode from podcast, there's no start or duration start/duration get set, instead, only from a playlist entry.
    duration:   0.0  // TODO: get the duration if it's supplied by enclosure of podcast for now, clear it, just like for 'start' above.
  }
  playlist_changed = true;
  playlist_appended = true;
}
/****************************************************************************
 * Actually, this addes new entry to end of playlist, therefore player_AddEndPlaylist()
 * not needed, what's needed as options are insert before current position, and
 * insert at head of playlist....
 ****************************************************************************/
function player_PlayNow_AddPlaylist(callingWidget) {
  if (plentry_len == null) {
    plentry_len = 0;
  }
  else {
    if (player_posn) { //save latest position/duration of current
      playlist[plentry_idx].duration = player_durn;
      switch (player_status_cur) {
      case 'COMPLETED': //if COMPLETED, position is end, IDLE means never played
      case 'IDLE':
	playlist[plentry_idx].start = 0.0; //then set .start to beginning.
	playlist_changed = true;
	break;
//    case 'BUFFERING': case 'PLAYING': case 'PAUSED': // if was playing, continue playing where left off
      default: 
	playlist[plentry_idx].start = player_posn;  //set start to wherever it was
	playlist_changed = true;
	break;
      }

    }
  }
  player_extract_from_episode_lens_button(callingWidget); //extract info into new last elt playlist[plentry_len]
  insertTailPlaylist();	//take items extracted into playlist[plentry_len] and insert into Exhibit DB
  plentry_idx = plentry_len; //PLAY NOW: new current item position is last item
  plentry_len++;

  statusMsg("player_PlayNow_AddPlaylist("
	    + callingWidget
	    + "): added '"
	    + playlist[plentry_len-1].file
	    + "': "
	    + plentry_len
	    + " entry/ies in playlist.");

  // switch to player in lower viewpanel
  if (switchView("podcasts-viewpanel", 4)) { //not displaying? 
    player_init_auto(); //then init player, init_jspl_and_listeners() fires, sets the_jw_obj, and call player_load_and_play(true)
  }
  else { //else, switchView() says already displaying player panel, which means the_jw_obj is current player: just call player_load_and_play()
    player_load_and_play(true);
  }
}
/****************************************************************************
 * add item to end of playlist, current item remains selected and/or playing
 ****************************************************************************/
function player_AddPlaylist(callingWidget) {
  if (plentry_len == null) {
    plentry_len = 0;
  }
  player_extract_from_episode_lens_button(callingWidget); //extract info into new last elt playlist[plentry_len]
  insertTailPlaylist();		// take items extracted into playlist[plentry_len-1] and insert into Exhibit DB
  plentry_len++;

  switch (player_status_cur) {
  case 'COMPLETED':
  case 'IDLE': 
    statusMsg("Added '"
	      + playlist[plentry_len-1].file
	      + "' to "
	      + plentry_len
	      + " entry/ies in playlist.");
    //playlist[plentry_idx].start = playlist[plentry_idx].duration; //then set .start to end
    //playlist_changed = true;
    player_load_and_play(true);	// load the updated playlist, set current to same as last, but don't play
  case 'PAUSED':	    //finished, paused, or never started playing...
    statusMsg("Added '"
	      + playlist[plentry_len-1].file
	      + "' to "
	      + plentry_len
	      + " entry/ies in playlist (start player to hear new entry).");
    player_load_and_play(false);  // load the updated playlist, set current to same as last, but don't play
    break;
  case 'BUFFERING':
  case 'PLAYING':  // playing -- update player on next track
    statusMsg("Added '"
	      + playlist[plentry_len-1].file
	      + "': "
	      + plentry_len
	      + " entry/ies in playlist (update waiting for current item playback to end).");
    // don't update the playlist, wait till player_item_listener() fires...
    // playlist[plentry_idx].start = player_posn;  //set start to wherever it was
    // player_load_and_play(true);
    break;
  }
}
/****************************************************************************
 * TODO, but don't bother This is badly named, what's needed as options are
 * insert before current position, and insert at head of playlist....
 ****************************************************************************/
function player_AddEndPlaylist(callingWidget) {
  statusMsg("TODO: player_AddEndPlaylist(" + callingWidget + "): "
	    + get_media_from_episode_lens_button(callingWidget));
  // TODO: implement
}
/****************************************************************************
 * 
 ****************************************************************************/
function clearAllFeedsInfo() {
  //feed_map            = new Hash(); //http://www.prototypejs.org/api/hash
  current_feed_episodes = [];
  current_feed_length = null;
  current_feed_idx    = null;
}
/****************************************************************************
 * switch viewpanels if not already showing, returning true. if already
 * showing return false.
 ****************************************************************************/
function switchView(viewpanel_name, idx_in_viewpanel) {
  var viewpanel = exhibit.getComponent(viewpanel_name);
  if (viewpanel) {
    if (viewpanel._viewIndex != idx_in_viewpanel) {
      viewpanel._switchView(idx_in_viewpanel);
      return true;
    }
    else { 
      return false;
    }
  }
  else {
    alert("switchView('" + viewpanel_name + "'," + idx_in_viewpanel + "): viewpanel not found - '" +  viewpanel_name + "'.");
  }
}
/****************************************************************************
 * Loses all dynamically loaded data from browsing, then reloads initial 
 * static data. TODO: Consider caching feed data so that multiple users 
 * can share the same information rather than requesting an individual copy
 * per browser/user. Also, consider saving .fetchedImg -> .img for cases
 * where .img isn't defined
 ****************************************************************************/
function removeAllFeedEpisodesFromExhibitDB() {
  // first, need to clear any selections in "episodes-hierarchichal-facet"
  // if not done, the summary checkbox in CSS-class "exhibit-facet-header-filterControl"
  // doesn't get cleared and continues to indicate a selection even after the db
  // has been cleared.
  var episode_facet = exhibit.getComponent("episodes-hierarchichal-facet");
  if (episode_facet.hasRestrictions()) {
     episode_facet.clearAllRestrictions();
  }
  exhibit.getDatabase().removeAllStatements();
  exhibit.getDatabase().loadDataLinks(function(){});
  exhibit.getDatabase().loadData(data);
}
/****************************************************************************
 * 
 ****************************************************************************/
function addFeedEpisodesToExhibitDB(feed_episodes) {
    if (feed_episodes.length > 0) {  //if there's any entries in feed, there'll be a minimum of two, one for feed, one for Podcast episodeList
      current_feed_episodes = feed_episodes;
      current_feed_length = current_feed_episodes.length - 1; //subtract one entry for the Podcast episodeList update
      // load new entries into exhibit database.
//    exhibit.getDatabase().loadItems(current_feed_episodes, "$xwiki.getURL("$doc.fullName", "view", "")");##
      exhibit.getDatabase().loadItems(current_feed_episodes, "???"); // TODO qtzibit
      // set "cursor" to latest feed entry 
      if (current_feed_length > 0) { // note that on failure of Feed3ExhibitJSON, it will still output 1 entry to update the podcast w/ fetchFailed=true.
        current_feed_idx = 0; //assumption: 0th-entry, first in feed, is latest.
      }
    }
    else {
      clearAllFeedsInfo();
    }
}
/****************************************************************************
 *  one click two click red click, blue click....
 ****************************************************************************/
function podcast_oneclick(podcastID) {
  fetchPodcastFeed(podcastID, false); /* false: value passed to fetchPodcastDone( ... selectedP=false ...) */
}
function podcast_twoclick(podcastID) {
  fetchPodcastFeed(podcastID, true);  /* true: value passed to fetchPodcastDone( ... selectedP=true ...) */
}
function episode_oneclick(episodeID) {
  podcastProducerFacetSelect(exhibit.getDatabase().getObject(episodeID, "podcast"),
			     false); //false->single click
}
function episode_twoclick(episodeID) {
  podcastProducerFacetSelect(exhibit.getDatabase().getObject(episodeID, "podcast"),
			     true); //true->double click
}
/****************************************************************************
 * podcastProducerFacetSelect() - focus the producer facet on the given producerID.
 ****************************************************************************/
function podcastProducerFacetSelect(podcastID, doubleclickP) { 
  if (podcastID) {
    var producerID = exhibit.getDatabase().getObject(podcastID, "producer");
    if (doubleclickP) {
      // switch to the selected thumbnails
      switchView("podcasts-viewpanel", 0);
      statusMsg("Showing podcasts from producer '" 
		+ producerID 
		+ "' (to filter w/o showing, try single-click)");
    }
    else { // single click doesn't call switchView()
      statusMsg("Filtering podcasts to producer '" 
		+ producerID 
		+ "' (to show icons, try double-click)");
    }
    exhibit.getComponent("podcasts-producer-facet").applyRestrictions
      ( { selection: [ producerID ], selectMissing : false } );
  }
}
/****************************************************************************
 * podcastTopicFacetSelect() - focus the topic facet on the given topic
 * doubleclickP: ignored for now, single/double click actions are same
 ****************************************************************************/
function podcastTopicFacetSelect(podcastID, doubleclickP) { 
  if (podcastID) {
    var topic = exhibit.getDatabase().getObject(podcastID, "topic");
    // TODO
  }
}
/****************************************************************************
 * <button type="button" class="thumbtn" onclick="processEpisodeThumBtnClick(this);">
 ****************************************************************************/
var clickhash = new Object();
function processEpisodeThumBtnClick(callingWidget) {
  /*
   * the SPAN parent of button w/ onclick that called processEpisodeThumBtnClick(),
   * created by Episodes-collection thumbnail-view, is given attribute ex:itemID,
   * the value of which is the episodeID. path from bubble-containing itemID to 
   * button: span/table/tbody/tr/td/button
   */
  var episodeID = callingWidget.parentNode.parentNode.parentNode.parentNode.parentNode.getAttribute("ex:itemID");
  var oldclick = clickhash[episodeID];
  /*
   * was button already clicked once within 200ms timeout period? 
   * (nb: first time you get "undefined" -- there's no hash at
   * all). Only once defined, you can test for the timer being clear == 0.
   * !(oldclick == null && oldclick !== null) is a hacky way of testing for undefined
   * suggested in http://constc.blogspot.com/2008/07/undeclared-undefined-null-in-javascript.html
   */
  if (!(oldclick == null && oldclick !== null) && (oldclick != 0)) {
    clearTimeout(clickhash[episodeID]);        //clear the previously schedule episode_oneclick()
    clickhash[episodeID] = 0;
    episode_twoclick(episodeID);               //fire episode_twoclick() on doubleclick
  }
  /*
   * else, not already clicked within 200ms timeout period,
   * or fall-thru to here if not in hash at all (first time).
   */
  else {
    clickhash[episodeID] //create timeout that fires the episode_oneclick() after 200ms timeout waiting for a possible double-click
       = setTimeout("clickhash['" +  episodeID + "'] = 0 ; episode_oneclick('" + episodeID + "');",
                    200); // Make this value higher to allow slower double clicks
    // if no following click occurs, the above eventually fires episode_oneclick("'" + episodeID + "'"); 
  }
  return false;
}
/****************************************************************************
 * <button type="button" class="thumbtn" onclick="processPodcastThumBtnClick(this);">
 * processPodcastThumBtnClick() called via 
 * attempt to get exhibit object from callingWidget param
 * used suggestion from http://www.webxpertz.net/forums/showthread.php?threadid=2188
 * which didn't work until i doubled the technique and got it to eat both single
 * clicks that happen alongside a double click.
 ****************************************************************************/
function processPodcastThumBtnClick(callingWidget) {
  /*
   * the SPAN parent of button w/ onclick that called processPodcastThumBtnClick(),
   * created by Podcasts-collection thumbnail-view, is given attribute ex:itemID,
   * the value of which is the podcastID
   */
  var podcastID = callingWidget.parentNode.getAttribute("ex:itemID"); 
  var oldclick = clickhash[podcastID];
  /*
   * was button already clicked once within 200ms timeout period? 
   * (nb: first time you get "undefined" -- there's no hash at
   * all). Only once defined, you can test for the timer being clear == 0.
   * !(oldclick == null && oldclick !== null) is a hacky way of testing for undefined
   * suggested in http://constc.blogspot.com/2008/07/undeclared-undefined-null-in-javascript.html
   */
  if (!(oldclick == null && oldclick !== null) && (oldclick != 0)) {
    clearTimeout(clickhash[podcastID]);        //clear the previously schedule podcast_oneclick()
    clickhash[podcastID] = 0;
    podcast_twoclick(podcastID);               //fire podcast_twoclick() on doubleclick
  }
  /*
   * else, not already clicked within 200ms timeout period,
   * or fall-thru to here if not in hash at all (first time).
   */
  else {
    clickhash[podcastID] //create timeout that fires the podcast_oneclick() after 200ms timeout waiting for a possible double-click
       = setTimeout("clickhash['" +  podcastID + "'] = 0 ; podcast_oneclick('" + podcastID + "');",
                    200); // Make this value higher to allow slower double clicks
    // if no following click occurs, the above eventually fires podcast_oneclick("'" + podcastID + "'"); 
  }
  return false;
}
/****************************************************************************
 * Asynchronously browse a feed via via Macros.Feed3ExhibitJSON. 
 * This function was once called out of Exhibit Lens Template for collection "Podcasts":
 * <a ex:href-subcontent='javascript:fetchPodcastFeed("{{.feed}}", "{{.label}}", {{value}}, "{{.episodeList}}");'>...
 * now it's called from a BUTTON's onclick or ondblclick and looks up all
 * necessary exhibit info out of the JSON db  via db.getObject(podcastID, ...).
 ****************************************************************************/
function fetchPodcastFeed(podcastID, selectedP) {
  var db = exhibit.getDatabase();
  if (db.getObject(podcastID, "episodeList") != null) { // if episodeList!=[], this value passed as "null" else the epidodeID
    // non-null episodeList means the feed has been fetched previously. In this case,
    // we ensure the already-fetched feed is displayed. on double-click, selectedP=true, and
    // indicates the feed is to be selected exclusively in the timeline to help focus
    // on the new feed. If selectedP=false (single-click), the new feed information is added
    // alongside existing selections. (so as not to inadvertently destroy users selections).
    // users wanting to lose their previous selections and focus on the new ones should 
    // double-click....
    var episode_facet = exhibit.getComponent("episodes-hierarchichal-facet");
    var already_selected = false;
    if (selectedP) {
      statusMsg("Focusing on '"
		+ db.getObject(podcastID, "label")
		+ "' (try single click to add).");
      already_selected = true;
      episode_facet._selections = []; //like clearAllRestrictions() w/o call to _notifyCollection()
      episode_facet._selections.push( { value: podcastID, selectOthers: false } );
      episode_facet._notifyCollection();
      window.scrollTo(0, 100); // make the timeline visible incase window scrolled down showing many thumbnails
    }
    else {
      if (episode_facet.hasRestrictions()) {
	for (var i = 0 ; (i < episode_facet._selections.length) ; i++) {
	  if (episode_facet._selections[i].value == podcastID) {
	    already_selected = true;
	    statusMsg("Already showing '"
		      + db.getObject(podcastID, "label")
		      + "' (try double click to focus).");
	  }
	}
      }
    }
    if (!already_selected) {
      statusMsg("Adding '"
		+ db.getObject(podcastID, "label")
		+ "' (try double click to focus).");
      episode_facet._selections.push( { value: podcastID, selectOthers: false } );
      episode_facet._notifyCollection();
    }
  }
  else {
    statusPodcastLoading(podcastID);
    // NOTE: injectJS() asynchronously updates current_feed_episodes[] and calls fetchPodcastDone()
//  injectJS('/xwiki/bin/view/Macros/Feed3ExhibitJSON?xpage=plain&podcast='
    console.log("TODO: Browse Feed "
	     + podcastID
             + '&selected='
             + selectedP
	     + '&url='
	     + db.getObject(podcastID, "feed"));
  }
}
/****************************************************************************
 * This case called, asynchronously, out of Macros.Feeds3JS after 
 * injectJS('/xwiki/bin/view/Macros/Feed3ExhibitJSON?xpage=plain&url=...') below
 * episodes[] is a list of exhibit episode-id's, addFeedEpisodesToExhibitDB() inserts
 * them into the Exhibit database and updates local vars for the current feed.
 ****************************************************************************/
function fetchPodcastDone(podcastID, selectedP, episodes) {
  addFeedEpisodesToExhibitDB(episodes); // init new current_feed_episodes, current_feed_length; clears if empty (e.g from error)
  if (current_feed_length > 0) {
    var episode_facet = exhibit.getComponent("episodes-hierarchichal-facet");
    var already_selected = false;
    if (selectedP) {
      statusMsg("Focusing "
                + current_feed_length 
                + (current_feed_length == 1 ? ' episode' : ' episodes') 
                + ' in "'
                + exhibit.getDatabase().getObject(podcastID, "fetchedTitle")
                + "' (try single click to add)");
      already_selected = true;
      episode_facet._selections = []; //like clearAllRestrictions() w/o call to _notifyCollection()
      episode_facet._selections.push( { value: podcastID, selectOthers: false } );
      episode_facet._notifyCollection();
      window.scrollTo(0,100); // make the timeline visible incase window scrolled down showing many thumbnails
    }
    else {
      if (episode_facet.hasRestrictions()) {
	for (var i = 0 ; (i < episode_facet._selections.length) ; i++) {
	  if (episode_facet._selections[i].value == podcastID) {
	    already_selected = true;
	    statusMsg("Refreshing '" 
		      + exhibit.getDatabase().getObject(podcastID, "fetchedTitle")
		      + "'");
	  }
	}
      }
    }
    if (!already_selected) {
      statusMsg("Adding "
                + current_feed_length 
                + (current_feed_length == 1 ? ' episode' : ' episodes') 
                + ' in "'
                + exhibit.getDatabase().getObject(podcastID, "fetchedTitle")
                + "' (try double click to focus).");
      episode_facet._selections.push( { value: podcastID, selectOthers: false } );
      episode_facet._notifyCollection();
    }
    // switch to timeline pane in upper viewpanel
    switchView("episodes-viewpanel", 1);
  }
  else {
    errorMsg('Error browsing feed ' 
             + exhibit.getDatabase().getObject(podcastID, "label")
             + ' or feed returned zero episodes...');
  }
}
/****************************************************************************
 * called out of fetchPodcastFeed() when a feed is browsed in Exhibit.
 ****************************************************************************/
function statusPodcastLoading(podcastID) {
//exhibit.getComponent("episodes-timeline-view")._timeline.showLoadingMessage();
  var lbl_elt = document.getElementById("status_msg");
  var img_elt = document.getElementById("status_img");
  lbl_elt.innerHTML   = 'Processing: feed "'
    + exhibit.getDatabase().getObject(podcastID, "label")
    + '"...';
//lbl_elt.style.color = "Black";
//img_elt.src         = '$loading_img';
  img_elt.src         = 'icons/spinner.gif';
} 
/****************************************************************************
 * Display a message in status area.
 * TODO: use a timer to make the message clear after 30 secs
 ****************************************************************************/
function statusMsg(msg_str) {
  var lbl_elt = document.getElementById("status_msg");
  var img_elt = document.getElementById("status_img");
  lbl_elt.innerHTML   = msg_str;
//lbl_elt.style.color = color;
//img_elt.src         = "$instruction_img";
  img_elt.src         = "icons/msginfo.png";
}
/****************************************************************************
 * Display an error in status area.
 * TODO: use a timer to make the message clear after 30 secs
 ****************************************************************************/
function errorMsg(msg_str) {
  var lbl_elt = document.getElementById("status_msg");
  var img_elt = document.getElementById("status_img");
  lbl_elt.innerHTML   = msg_str;
//lbl_elt.style.color = color;
//img_elt.src         = "$error_img";
  img_elt.src         = "icons/msgerror.png";
}
/****************************************************************************
 * Display a warning in status area.
 * TODO: use a timer to make the message clear after 30 secs
 ****************************************************************************/
function warningMsg(msg_str) {
  var lbl_elt = document.getElementById("status_msg");
  var img_elt = document.getElementById("status_img");
  lbl_elt.innerHTML   = msg_str;
//lbl_elt.style.color = color;
//img_elt.src         = "$warning_img";
  img_elt.src         = "icons/msgwarning.png";
}
/****************************************************************************
 * This case called out of clearFeeds() when "Clear Timeline" button is clicked
 ****************************************************************************/
function feedStatusClear() {
  clearAllFeedsInfo();
  statusMsg("Select a feed below, view by clicking on feed entry in Timeline above...");
}
/****************************************************************************
 * clear all feed entries in timeline
 ****************************************************************************/
function clearFeeds() {
  feedStatusClear();
//exhibit.getComponent("episodes-timeline-view")._timeline.showLoadingMessage();
//exhibit.getComponent("episodes-timeline-view")._eventSource.clear();
//exhibit.getComponent("episodes-timeline-view")._timeline.hideLoadingMessage();
}
/****************************************************************************
 * 
 ****************************************************************************/
function latestFeedEntry() {
  if (current_feed_idx == null) {
     return;
  }
  // assumption: 0th-entry, first in feed, is latest.
  current_feed_idx = 0;
  exhibit.getComponent("episodes-timeline-view")._timeline.getBand(0).setCenterVisibleDate(current_feed_episodes[current_feed_idx].entryTime);
}
/****************************************************************************
 * 
 ****************************************************************************/
function laterFeedEntry() {
  if (current_feed_idx == null) {
     return;
  }
  if (current_feed_idx > 0) {
     current_feed_idx--;
  }
  exhibit.getComponent("episodes-timeline-view")._timeline.getBand(0).setCenterVisibleDate(current_feed_episodes[current_feed_idx].entryTime); 
}
/****************************************************************************
 * 
 ****************************************************************************/
function earlierFeedEntry() {
  if (current_feed_idx == null) {
     return;
  }
  if (current_feed_idx < (current_feed_length - 1)) {
     current_feed_idx++;
  }
  exhibit.getComponent("episodes-timeline-view")._timeline.getBand(0).setCenterVisibleDate(current_feed_episodes[current_feed_idx].entryTime); 
}
/****************************************************************************
 * 
 ****************************************************************************/
function earliestFeedEntry() {
  if (current_feed_length == null) {
     return;
  }
  // assumption: last entry in feed is earliest
  current_feed_idx = current_feed_length - 1; 
  exhibit.getComponent("episodes-timeline-view")._timeline.getBand(0).setCenterVisibleDate(current_feed_episodes[current_feed_idx].entryTime); 
}
